Praca domowa nr 2 dotyczy metody wyjaśnień lokalnych LIME - Local Interpretable Model-Agnostic Explanations, która (podobnie jak metody Break Down i SHAP z poprzedniej pracy domowej) odpowiada na pytanie, jakie zmienne miały wpływ na otrzymaną predykcję wybranej obserwacji.
import pandas as pd
import numpy as np
import pickle
import dalex as dx
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
import warnings
warnings.filterwarnings('ignore')
# Wczytanie i przygotowanie danych
full_data = pd.read_csv("data/hotel_bookings.csv")
full_data["agent"] = full_data["agent"].astype(str)
treshold = 0.005 * len(full_data)
agents_to_change = full_data['agent'].value_counts()[full_data['agent'].value_counts() < treshold].index
full_data.loc[full_data["agent"].isin(agents_to_change), "agent"] = "other"
countries_to_change = full_data['country'].value_counts()[full_data['country'].value_counts() < treshold].index
full_data.loc[full_data["country"].isin(countries_to_change), "country"] = "other"
# Określenie cech uwzględnionych w modelu
num_features = ["lead_time", "arrival_date_week_number",
"stays_in_weekend_nights", "stays_in_week_nights",
"adults", "previous_cancellations",
"previous_bookings_not_canceled",
"required_car_parking_spaces", "total_of_special_requests",
"adr", "booking_changes"]
cat_features = ["hotel", "market_segment", "country",
"reserved_room_type",
"customer_type", "agent"]
features = num_features + cat_features
# Podział na zmienne wyjaśniające i target
X = full_data.drop(["is_canceled"], axis=1)[features]
y = full_data["is_canceled"]
categorical_names = {}
for feature in cat_features:
col = X[[feature]]
cat_transformer = SimpleImputer(strategy="constant", fill_value="Unknown")
col = cat_transformer.fit_transform(col)
X[feature] = col
le = LabelEncoder()
le.fit(X[[feature]])
X[[feature]] = le.transform(X[[feature]])
categorical_names[feature] = le.classes_
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, random_state=42)
Nastąpiło kilka zmian w porównaniu do modelu wykorzystanego w poprzedniej pracy domowej. Musieliśmy zastosować label encoding zmiennych kategorycznych. Stąd w powyższej sekcji w przygotowaniu danych obecne są operacje, które umożliwiają stworzenie słownika z wartościami odpowiadającymi poszczególnym kodom - categorical_names.
Sam nowy model wczytuję z pickle'a, a odpowiedniego notebooka dotyczącego modelowania można znaleźć już na repo.
rf_model = pickle.load(open("RF_model_le", 'rb'))
explainer = dx.Explainer(rf_model, X_train, y_train)
Weźmy pod uwagę 2000-czną obserwację w zbiorze treningowym. Spójrzmy, jak wyglądają wartości w poszczególnych kolumnach.
X_train.iloc[2000]
country = 16.0 nie mówi zbyt wiele, dlatego przygotujemy funkcję, która będzie zwracać tekstowe wartości zmiennych dla wybranych obserwacji. Pomoże to też lepiej zrozumieć otrzymywane wyjaśnienia.
def get_cat_names(x):
for feature in cat_features:
print(f"{feature}: {categorical_names[feature][int(x[feature])]}")
get_cat_names(X_train.iloc[2000])
print(f"Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {rf_model.predict(X_train)[2000]}, \nPrawidłowa klasyfikacja to: {y_train.iloc[2000]}.")
Dla wybranej obserwacji model przewiduje target = 1, co oznacza, że rezerwacja zostanie odwołana. Jest to rzeczywiście odpowiednia wartość - rezerwacja ta jest oznaczona w zbiorze jako odwołana.
# Potrzebne do adnotowania, które kolumny powinny być traktowane przez LIME jako kategoryczne
cat_features_columns = [X_train.columns.get_loc(c) for c in cat_features]
ps = explainer.predict_surrogate(X_train.iloc[2000,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps.show_in_notebook()
ps.plot()
get_cat_names(X_train.iloc[2000,:])
1.0 - model był jej prawie pewny. Oprócz otrzymanej już wyżej dekompozycji, weźmiemy pod uwagę obserwacje o różnych predykcjach i różnych pewnościach tych predykcji, a nawet jedną źle zaklasyfikowaną.
pd.DataFrame({"Prediction" : rf_model.predict(X_train.iloc[[3000, 2300, 4242, 83, 1410]]), "Target": y_train.iloc[[3000, 2300, 4242, 83, 1410]]})
ps2 = explainer.predict_surrogate(X_train.iloc[3000,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps2.show_in_notebook()
ps3 = explainer.predict_surrogate(X_train.iloc[2300,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps3.show_in_notebook()
ps4 = explainer.predict_surrogate(X_train.iloc[4242,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps4.show_in_notebook()
ps5 = explainer.predict_surrogate(X_train.iloc[83,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps5.show_in_notebook()
ps6 = explainer.predict_surrogate(X_train.iloc[1410,:], type = "lime", class_names=['not canceled', 'canceled'],
categorical_names = categorical_names, categorical_features = cat_features_columns)
ps6.show_in_notebook()
ps6.plot()
get_cat_names(X_train.iloc[2300,:])
get_cat_names(X_train.iloc[83,:])
previous_cancellations, która informuje o ilości wcześniej odwołanych rezerwacji przez danego klienta. Co więcej, dla tych samych wartości wkłady w predykcje są bardzo podobne lub takie same (brak wcześniejszych odwołań daje 0.35 w stronę nieodwołania). required_car_parking_spaces. Podobnie jak wyżej - dla tej samej wartości daje ona bardzo podobne wyniki kontrybucji w predykcji. country. Zauważalne jest, że odgrywa ona tu jednak mniejszą rolę niż w przypadku dekompozycji Break Down. Wartym zauważenia jest to, że pochodzenie z Portugalii ma znacznie większy wpływ ze względu na wartość niż z innych krajów (w przykładach Włochy i Niderlandy). previous_bookings_not_canceled <=0 i >0 (z ostatniej obserwacji). Może to być sprzeczne z intuicją, bo jedna nieodwołana wcześniej obserwacja, a 10 jak w tym przykładzie robi z pozoru różnicę. .plot() zamiast .show_in_notebook().